﻿using System;
using System.Threading.Tasks;
using Xamarin.Forms;
using System.Linq;
using System.Windows.Input;
using System.ComponentModel;
using System.Reflection;

namespace Moven.Behaviors
{
	/// <summary>
	/// Curtesy of the article about Attached Behavior at 
	/// http://blog.masterdevs.com/xf-day-7/
	/// Added support for tapping animation plus focus, also added Duration property, simplified 
	/// GetEasing code a bit, change obsolete code
	/// 
	/// TODO:
	/// #1, Handle Tapped event for Button and other tappable controls etc, maybe we don't need to 
	/// because internally maybe they are using gesture recongnizer to do tapping handling
	/// #2, Make the event configurable, probably use Enum, or just command name
	/// #3, Make the gesture recongizer configurable, tis weill make it extensible
	/// #4, Make the animation itself configurable, but that's maybe too far, because it will change 
	///     the original purpose of the class, but maybe not, we'll see
	/// </summary>
	public class AnimateSizeBehavior : Corcav.Behaviors.Behavior<View>
	{
		static readonly Type SelfType = typeof(AnimateSizeBehavior);

		public static readonly BindableProperty ScaleProperty = BindableProperty.Create(
			nameof(Scale),
			typeof(double),
			SelfType,
			1.25);

		public static readonly BindableProperty EasingFunctionNameProperty = BindableProperty.Create(
			nameof(EasingFunctionName),
			typeof(string),
			SelfType,
			propertyChanged: OnEasingFunctionChanged);

		public const int MinDuration = 50;

		public static readonly BindableProperty DurationProperty = BindableProperty.Create(
			nameof(Duration),
			typeof(int),
			SelfType,
			200);

		Easing _easingFunction;

		public string EasingFunctionName
		{
			get { return (string)GetValue(EasingFunctionNameProperty); }
			set { SetValue(EasingFunctionNameProperty, value); }
		}

		public double Scale
		{
			get { return (double)GetValue(ScaleProperty); }
			set { SetValue(ScaleProperty, value); }
		}

		public int Duration
		{
			get { return (int)GetValue(DurationProperty); }
			set 
			{
				if (value < MinDuration)
				{
					return;
				}
				SetValue(DurationProperty, value); 
			}
		}

		TapGestureRecognizer TapGestureRecognizer => AssociatedObject.GestureRecognizers.
										 FirstOrDefault(x => x is TapGestureRecognizer)
															 as TapGestureRecognizer;
		ICommand _originalTapCommand, _newTapCommand;

		ICommand NewTapCommand => _newTapCommand?? (_newTapCommand = new Command(
						async parameter =>
						{
							_originalTapCommand.Execute(parameter);
							await Animate();
						}, parameter => _originalTapCommand.CanExecute(parameter)));


		void AttachTapCommandOrEvent(TapGestureRecognizer recognizer)
		{
			if (recognizer.Command != null)
			{
				_originalTapCommand = recognizer.Command;
				recognizer.Command = NewTapCommand;
			}
			else
			{
				recognizer.Tapped += OnItemTapped;
			}
		}

		protected override void OnAttach()
		{
			AssociatedObject.Focused += OnItemFocused;
			var gestureRecognizer = TapGestureRecognizer;
			if (gestureRecognizer != null)
			{
				gestureRecognizer.PropertyChanged += (object sender, PropertyChangedEventArgs e) => {
					if (e.PropertyName == nameof(gestureRecognizer.Command) && _originalTapCommand == null)
					{
						AttachTapCommandOrEvent(gestureRecognizer);
						gestureRecognizer.Tapped -= OnItemTapped;
					}
				};

				AttachTapCommandOrEvent(gestureRecognizer);
			}
		}

		protected override void OnDetach()
		{
			AssociatedObject.Focused -= OnItemFocused;
			var gestureRecognizer = TapGestureRecognizer;

			if (gestureRecognizer != null && _originalTapCommand == null)
			{
				gestureRecognizer.Tapped -= OnItemTapped;
			}
		}

		static readonly Type EasingType = typeof(Easing);
		static readonly BindingFlags FieldFlags = BindingFlags.Public | BindingFlags.Static;
		static readonly string[] ValidEasingNames = EasingType.GetFields(FieldFlags).
		                                                          Select(x => x.Name).ToArray();
		static Easing GetEasing(string easingName)
		{
			if (ValidEasingNames.Contains(easingName))
			{
				var easingFunc = (Easing)EasingType.GetField(easingName, FieldFlags).GetValue(null);
				return easingFunc;
			}
			throw new ArgumentException(easingName + " is not valid");
		}

		static void OnEasingFunctionChanged(BindableObject bindable, object oldValue, object newValue)
		{
			var easingFuncName = newValue as string;
			(bindable as AnimateSizeBehavior).EasingFunctionName = easingFuncName;
			(bindable as AnimateSizeBehavior)._easingFunction = GetEasing(easingFuncName);
		}

		async void OnItemFocused(object sender, FocusEventArgs e)
		{
			await Animate();
		}

		async void OnItemTapped(object sender, EventArgs e)
		{
			await Animate();
		}

		async Task Animate()
		{
			var duration = (uint)(Duration / 2);
			try
			{
				await AssociatedObject.ScaleTo(Scale, duration, _easingFunction);
				await AssociatedObject.ScaleTo(1.00, duration, _easingFunction);
			}
			catch
			{
			}
		}
	}
}